package com.gmrz.uaf.png;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageOutputStream;

import org.apache.commons.lang3.text.WordUtils;

import com.google.common.base.Splitter;
import com.gmrz.uaf.common.UAFRuntimeException;

public class TextToGraphics {
	private static final Font DEFAULT_FONT = new Font("SansSerif", 0, 12);
	private static final Color DEFAULT_FG = Color.RED;
	private static final Color DEFAULT_BG = Color.CYAN;
	private static byte[][] GRAY2 = (byte[][]) null;
	private static byte[][] GRAY4 = (byte[][]) null;

	private Font font = null;
	private Color foregroundColor = null;
	private Color backgroundColor = null;
	private BufferedImage header = null;

	int width = 100;
	int height = 100;

	int bitDepth = 1;
	int colourType = 1;
	byte[][] palette = (byte[][]) null;

	int compression = 0;
	int filter = 0;
	int interlace = 0;

	public TextToGraphics(int width, int height, int bitDepth, int colourType, byte[][] p) {
		this(width, height, bitDepth, colourType, p, DEFAULT_FONT, DEFAULT_FG, DEFAULT_BG, null);
	}

	public TextToGraphics(int width, int height, int bitDepth, int colourType, byte[][] p, Font f, Color fgcolor,
			Color bgcolor, BufferedImage header) {
		this.width = width;
		this.height = height;
		this.bitDepth = bitDepth;
		this.colourType = colourType;

		if ((p != null) && (p.length > 0) && (p[0] != null) && (p[0].length > 0) && (bitDepth == 16)) {
			this.bitDepth = 8;
		}

		clonePalette(p);

		this.font = f;
		this.foregroundColor = fgcolor;
		this.backgroundColor = bgcolor;
		this.header = header;
	}

	public byte[] convert2PNG(String text) throws IOException {
		validate();

		int breakCount = getMaxCharsFittingWidth(text);
		BufferedImage img = createBufferedImage();
		if (img != null) {
			Graphics2D g2d = img.createGraphics();
			g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
			g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
			g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);

			g2d.setFont(this.font);
			g2d.setColor(this.backgroundColor);
			g2d.fillRect(0, 0, this.width, this.height);
			g2d.setColor(this.foregroundColor);

			if (this.header != null) {
				g2d.drawImage(this.header, 0, 0, null);
			}

			String newLine = WordUtils.wrap(text, breakCount);
			Iterable<String> pieces = Splitter.on('\n').split(newLine);

			float y = 0.0F;
			Rectangle2D charBounds = this.font.getMaxCharBounds(g2d.getFontRenderContext());
			float charHeight = charBounds.getBounds().height;
			float charSpacing = 1.0F;

			for (String item : pieces) {
				TextLayout textLayout = new TextLayout(item, this.font, g2d.getFontRenderContext());
				y += charHeight;
				textLayout.draw(g2d, 0.0F, y);
				y += charSpacing;
			}
			g2d.dispose();
		}
		ImageTypeSpecifier typeSpecifier = createImageTypeSpecifier();
		ImageWriter writer = ImageIO.getImageWriters(typeSpecifier, "png").next();

		ImageWriteParam writeParam = writer.getDefaultWriteParam();
		IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);

		metadata = writer.convertImageMetadata(metadata, typeSpecifier, null);

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ImageOutputStream stream = ImageIO.createImageOutputStream(baos);
		if (stream != null) {
			writer.setOutput(stream);
			writer.write(metadata, new IIOImage(img, null, null), writeParam);
			stream.close();
		} else {
			throw new UAFRuntimeException("Stream object is null");
		}
		return baos.toByteArray();
	}

	private void validate() {
		if (this.filter != 0)
			throw new IllegalArgumentException("Invalid filter: " + this.filter);
		if (this.compression != 0)
			throw new IllegalArgumentException("Invalid compression: " + this.compression);
		if ((this.interlace != 0) && (this.interlace != 1))
			throw new IllegalArgumentException("Invalid interlace: " + this.interlace);
		if ((this.bitDepth != 1) && (this.bitDepth != 2) && (this.bitDepth != 4) && (this.bitDepth != 8)
				&& (this.bitDepth != 16))
			throw new IllegalArgumentException("Invalid bit depth value: " + this.bitDepth);
		if ((hasPalette()) && (this.palette[0].length > 256))
			throw new IllegalArgumentException("Palette larger than 256 entries not allowed");
		ColourType color = ColourType.valueOf(this.colourType);
		switch (color.ordinal() + 1) {
		case 1:
			if (((this.bitDepth == 8) || (this.bitDepth == 16)) && (hasPalette()))
				throw new IllegalArgumentException(
						"Palette not allowed for colour type " + color + " and bit depth: " + this.bitDepth);

			break;
		case 2:
			if ((this.bitDepth != 8) && (this.bitDepth != 16))
				throw new IllegalArgumentException("Invalid bit depth: " + this.bitDepth + "for colour type " + color);
			break;
		case 3:
			break;
		case 4:
			if ((this.bitDepth != 8) && (this.bitDepth != 16)) {
				throw new IllegalArgumentException("Invalid bit depth: " + this.bitDepth + "for colour type " + color);
			}
			if (hasPalette())
				throw new IllegalArgumentException("Palette not allowed for colour type " + color);
			break;
		case 5:
			if ((this.bitDepth != 8) && (this.bitDepth != 16)) {
				throw new IllegalArgumentException("Invalid bit depth: " + this.bitDepth + "for colour type " + color);
			}

			if (hasPalette())
				throw new IllegalArgumentException("Palette not supported for colour type " + color);
			break;
		}
	}

	private BufferedImage createImageFromPalette(int dataBufferType) {
		DataBuffer dataBuffer = null;
		switch (dataBufferType) {
		case 0:
			byte[] pixels = new byte[this.width * this.height];
			dataBuffer = new DataBufferByte(pixels, this.width * this.height, 0);
			break;
		case 3:
			int[] ints = new int[this.width * this.height];
			dataBuffer = new DataBufferInt(ints, this.width * this.height, 0);
			break;
		case 2:
			short[] shorts = new short[this.width * this.height];
			dataBuffer = new DataBufferShort(shorts, this.width * this.height, 0);
			break;
		case 1:
			short[] ushorts = new short[this.width * this.height];
			dataBuffer = new DataBufferUShort(ushorts, this.width * this.height, 0);
		}

		SampleModel sampleModel = new MultiPixelPackedSampleModel(dataBufferType, this.width, this.height,
				this.bitDepth);
		WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, null);
		IndexColorModel colorModel = new IndexColorModel(this.bitDepth, this.palette[0].length, this.palette[0],
				this.palette[1], this.palette[2]);
		return new BufferedImage(colorModel, raster, false, null);
	}

	private ImageTypeSpecifier createImageTypeSpecifier() {
		ColourType color = ColourType.valueOf(this.colourType);
		ImageTypeSpecifier typeSpecifier = null;
		switch (color.ordinal() + 1) {
		case 1:
			typeSpecifier = ImageTypeSpecifier.createGrayscale(this.bitDepth, 1, false);
			break;
		case 2:
			if ((this.bitDepth == 8) && (!hasPalette()))
				typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(1);
			else if ((this.bitDepth == 8) && (hasPalette()))
				typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(13);
			else if (this.bitDepth == 16)
				typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(8);
			else {
				throw new RuntimeException(
						"Color type and bit depth mismatched, color type: " + color + ", bit depth: " + this.bitDepth);
			}
			break;
		case 3:
			if ((this.bitDepth == 1) || (this.bitDepth == 2))
				typeSpecifier = ImageTypeSpecifier.createIndexed(this.palette[0], this.palette[1], this.palette[2],
						null, this.bitDepth, 0);
			else
				typeSpecifier = ImageTypeSpecifier.createIndexed(this.palette[0], this.palette[1], this.palette[2],
						null, this.bitDepth, 0);
			break;
		case 4:
			typeSpecifier = ImageTypeSpecifier.createGrayscale(this.bitDepth, 1, false, true);
			break;
		case 5:
			typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(2);
		}

		return typeSpecifier;
	}

	private BufferedImage createBufferedImage() {
		ColourType color = ColourType.valueOf(this.colourType);
		switch (color.ordinal() + 1) {
		case 1:
			switch (this.bitDepth) {
			case 1:
				return new BufferedImage(this.width, this.height, 12);
			case 2:
				createGrayPalette(2);
				return createImageFromPalette(0);
			case 4:
				createGrayPalette(4);
				return createImageFromPalette(0);
			case 8:
				return new BufferedImage(this.width, this.height, 10);
			case 16:
				return new BufferedImage(this.width, this.height, 11);
			}
		case 2:
			if (this.bitDepth == 8) {
				return this.palette != null ? createImageFromPalette(1) : new BufferedImage(this.width, this.height, 1);
			}
			if (this.bitDepth == 16) {
				return this.palette != null ? createImageFromPalette(1) : new BufferedImage(this.width, this.height, 8);
			}

		case 3:
			if (this.bitDepth < 8)
				return createImageFromPalette(0);
			if (this.bitDepth == 8)
				return createImageFromPalette(0);
		case 4:
			if (this.bitDepth == 16)
				return new BufferedImage(this.width, this.height, 11);
			if (this.bitDepth == 8) {
				return new BufferedImage(this.width, this.height, 10);
			}
		case 5:
			if (this.bitDepth == 8)
				return new BufferedImage(this.width, this.height, 2);
			if (this.bitDepth == 16) {
				return new BufferedImage(this.width, this.height, 2);
			}
			break;
		}
		return null;
	}

	private int getMaxCharsFittingWidth(String text) {
		BufferedImage imgtemp = new BufferedImage(this.width, this.height, 1);
		Graphics2D gtemp = imgtemp.createGraphics();
		gtemp.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		gtemp.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);

		boolean overlimit = true;
		int breakCount = text.length();
		while (overlimit) {
			TextLayout textLayout = new TextLayout(text.substring(0, breakCount), this.font,
					gtemp.getFontRenderContext());
			textLayout.draw(gtemp, 0.0F, 0.0F);
			Rectangle2D bounds = textLayout.getBounds();
			overlimit = bounds.getWidth() > this.width;
			if (overlimit) {
				breakCount = (int) (breakCount * (this.width / bounds.getWidth()));
			}
		}
		gtemp.dispose();
		return breakCount;
	}

	private void createGrayPalette(int bitDepth) {
		if (this.palette != null)
			return;

		if ((bitDepth == 2) && (GRAY2 != null)) {
			this.palette = GRAY2;
			return;
		}
		if ((bitDepth == 4) && (GRAY4 != null)) {
			this.palette = GRAY4;
			return;
		}
		int clength = 1 << bitDepth;
		this.palette = new byte[3][clength];
		int cindex = 256 / clength + 1;
		for (int i = 0; i < clength; i++) {
			byte c = (byte) (i * cindex);
			this.palette[0][i] = c;
			this.palette[1][i] = c;
			this.palette[2][i] = c;
		}
		if (bitDepth == 2)
			GRAY2 = this.palette;
		if (bitDepth == 4)
			GRAY4 = this.palette;
	}

	private void clonePalette(byte[][] p) {
		if ((p == null) || (p.length == 0))
			return;
		if ((p[0] == null) || (p[0].length == 0))
			return;
		int l = p[0].length;
		this.palette = new byte[3][l];
		for (int i = 0; i < l; i++) {
			this.palette[0][i] = p[0][i];
			this.palette[1][i] = p[1][i];
			this.palette[2][i] = p[2][i];
		}
	}

	private boolean hasPalette() {
		return (this.palette != null) && (this.palette.length > 0) && (this.palette[0] != null)
				&& (this.palette[0].length > 0);
	}

	public enum ColourType {
		GRAY(0), RGB(2), PALETTE(3), GRAYALPHA(4), RGBALPHA(6);

		private int colour = 0;

		ColourType(int c) {
			this.colour = c;
		}

		public static ColourType valueOf(int colour) {
			for (ColourType c : values()) {
				if (c.colour == colour)
					return c;
			}
			throw new IllegalArgumentException("Invalid colour type: " + colour);
		}

		public int getColour() {
			return this.colour;
		}
	}
}
